AWS IoT Core に Amazon API Gateway からメッセージを Publish する方法

AWS IoT Core に Amazon API Gateway からメッセージを Publish する方法

Clock Icon2024.08.28

はじめに

AWS IoT Core にデータを publish する場合、よく使われるのは MQTTS プロトコルですが HTTPS でも実行が可能です。

https://docs.aws.amazon.com/iot/latest/developerguide/protocols.html

また、Amazon API Gateway では、AWS サービス統合(AWS Service Proxy)により直接 AWS サービスへ REST API でリクエストすることができるので、もちろん AWS IoT Core へメッセージを Publish することもできます。
(2024 年 08 月現在で、HTTP API では AWS IoT Core は未サポートです)

20-http-publish-basic.png

よくある構成として、クラウド側(AWS IoT Core)からデバイスにメッセージを送りたい場合、Lambda を使って Publish することが多いですが、要件がシンプルな場合は API Gateway 経由で送ることで Lambda レスな構成にすることができます。

REST API の各種パラメーターと AWS IoT Core の HTTP エンドポイントの関係

AWS IoT Core に対してメッセージを Publish するときは、下記のフォーマット形式の HTTP メッセージ URL に POST します。

https://[IoT_data_endpoint]/topics/[url_encoded_topic_name]?qos=1

https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/http.html

そのため、API Gateway からメッセージを送る時は、上記のパスに送ることができるようにパスを変換して渡す必要があります。

これは、API Gateway の「統合リクエスト」にある「アクションタイプ」の設定で「パスオーバーライド」を使う ことで実現できます。イメージとしては /foo/bar というパスに対して API Gateway にメッセージを POST すると、topics/foo/bar というパス形式に変換されて AWS IoT Core の HTTP メッセージ URL に POST されると考えると分かりやすいです。

63-pathoverride.png

パターン別設定方法

それでは具体的に設定しながら動作を見ていきたいと思います。今回は下記の 3 パターンの設定方法を紹介します。

  1. Publish するトピックが固定の場合
  2. Publish するトピックを URL パスで任意に指定する場合
  3. Publish するトピックを クエリ文字列で任意に指定する場合

今回は トピック sensor/temperature という 2 階層の構造になっているトピックにメッセージを送る想定とします。

パターン 1. Publish するトピックが固定の場合

最初は一番単純な形です。あらかじめ Publish するトピックが決まっている場合、「URL パスパラメーター」 にトピック名をセットします。

sensor/temperature というトピックに送るのでトピックの階層別にパスパラメーターを topic_level_1topic_level_2 と指定して、それぞれに sensortemperature をセットします。

また、AWS IoT Core のエンドポイントに Publish する HTTP エンドポイントのフォーマットに合わせて、パスオーバーライドの設定を topics/{topic_level_1}/{topic_level_2} とセットします。

100-http-publish-static-topic-pattern.png

実際のマネジメントコンソール上の 「結合リクエスト」 の設定画面です。
(今回は、/publish の POST メソッドに対して設定しています)

50-static-topic-integration-request-settings.png

IAM Role は 次のようなポリシーのものをセットします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PostStaticTopic",
            "Effect": "Allow",
            "Action": [
                "iot:Publish"
            ],
            "Resource": [
                "arn:aws:iot:[YOUR_REGION]:[YOUR_AWS_ACCOUNT_ID]:topic/sensor/temperature"
            ]
        }
    ]
}

信頼関係は API Gateway から Publish するので次のようにします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Sid": "",
            "Principal": {
                "Service": "apigateway.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

パターン 1 の CloudFormation テンプレート

CloudFormation テンプレートは以下のとおりです。

apigateway-publish-to-iotcore-static-topic.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'API Gateway with AWS IoT Core integration without mapping template'

Parameters:
  AwsIotSubDomain:
    Type: String
    Default: ""
  TopicLevel1:
    Type: String
    Default: sensor
  TopicLevel2:
    Type: String
    Default: temperature
  Qos:
    Type: String
    Default: "'1'"

Resources:
  ApiGateway:
    Type: 'AWS::ApiGateway::RestApi'
    Properties:
      Name: 'IoTCoreIntegrationApiStaticParameters'
      Description: 'API for publishing messages to AWS IoT Core'
      EndpointConfiguration:
        Types:
          - REGIONAL

  ApiResource:
    Type: 'AWS::ApiGateway::Resource'
    Properties:
      RestApiId: !Ref ApiGateway
      ParentId: !GetAtt ApiGateway.RootResourceId
      PathPart: 'publish'

  ApiMethod:
    Type: 'AWS::ApiGateway::Method'
    Properties:
      RestApiId: !Ref ApiGateway
      ResourceId: !Ref ApiResource
      HttpMethod: POST
      AuthorizationType: NONE
      Integration:
        Type: AWS
        IntegrationHttpMethod: POST
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:${AwsIotSubDomain}.iotdata:path/topics/{topic_level_1}/{topic_level_2}'
        Credentials: !GetAtt IoTRole.Arn
        RequestParameters:
          integration.request.path.topic_level_1: !Join ["", ["'", !Ref TopicLevel1, "'"]]
          integration.request.path.topic_level_2: !Join ["", ["'", !Ref TopicLevel2, "'"]]
          integration.request.querystring.qos: !Ref Qos
        PassthroughBehavior: WHEN_NO_TEMPLATES
        IntegrationResponses:
          - StatusCode: 200
      MethodResponses:
        - StatusCode: 200

  ApiDeployment:
    Type: 'AWS::ApiGateway::Deployment'
    DependsOn: ApiMethod
    Properties:
      RestApiId: !Ref ApiGateway
      StageName: 'dev'

  IoTRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: apigateway.amazonaws.com
            Action: 'sts:AssumeRole'
      Policies:
        - PolicyName: IoTPublishPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: 'iot:Publish'
                Resource: !Sub 'arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/${TopicLevel1}/${TopicLevel2}'

Outputs:
  ApiEndpoint:
    Description: 'API Gateway endpoint URL for dev stage'
    Value: !Sub 'https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/dev/publish'

パターン 1 の動作確認

curl コマンドで手元の PC からリクエストを投げてみます。

curl -X POST "https://[YOUR-API-ID].execute-api.[YOUR-REGION].amazonaws.com/dev/publish?qos=1" -H "Content-Type: application/json" -d '{"data": "28.5"}'

AWS IoT Core 上のテストクライアントで sensor/temperature をサブスクライブしていれば、下記のようにメッセージを受信できました。

51-iot-core-test-client.png

API Gateway のコンソール上のテスト画面では、次のように「クエリ文字列」と「リクエスト本文」を指定する形になります。

52-static-topic-test.png

テストを実施すると次のようなログを確認できたので、正常に IoT Core のエンドポイントにメッセージを送信できていることが分かります。

Endpoint request URI: https://[YOUR-IOT-CORE-ENDPOINT].iot.ap-northeast-1.amazonaws.com/topics/sensor/temperature?qos=1

パターン 2. Publish するトピックを URL パスで任意に指定する場合

次は、送信するトピックを都度指定して送る場合です。

101-http-publish-url-path-pattern.png

最初に URL のリソースパスをトピックの階層の数だけ作成(/publish/{topic_path_1}/{topic_path_2})します。

53-resource-path.png

一方で、AWS IoT Core のエンドポイントに Publish できる「HTTP エンドポイント」のフォーマットに合わせて、パスオーバーライドの設定を topics/{topic_level_1}/{topic_level_2} とセットします。

54-path-override.png

また、URL パスパラメーター のtopic_level_1topic_level_2 のマッピング元は「メソッドリクエスト」のリクエストパスである topic_path_1topic_path_2 とするため、method.request.path.topic_path_1method.request.path.topic_path_2 を指定します。

  • URL パスパラメータ
名前 マッピング元
topic_level_1 method.request.path.topic_path_1
topic_level_2 method.request.path.topic_path_2
  • URL クエリ文字列パラメータ
名前 マッピング元
qos method.request.querystring.qos

55-integraion-request.png

  • リクエストパス
    • topic_path_1
    • topic_path_2

56-method-request.png

これにより、メッセージの POST 時に 2 階層のパスを任意に指定することで同じ階層のトピックにメッセージを Publish できます。トピックの階層構造は / で区切るので URL パスと同じ形式になり分かりやすいかと思います。

パターン 2 の CloudFormation テンプレート

CloudFormation のテンプレートは以下のとおりです。

apigateway-publish-to-iotcore-rest-api-path.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'API Gateway with AWS IoT Core integration without mapping template'

Parameters:
  AwsIotSubDomain:
    Type: String
    Default: ""

Resources:
  ApiGateway:
    Type: 'AWS::ApiGateway::RestApi'
    Properties:
      Name: 'IoTCoreIntegrationApiPath'
      Description: 'API for publishing messages to AWS IoT Core'
      EndpointConfiguration:
        Types:
          - REGIONAL

  ApiResourcePublish:
    Type: 'AWS::ApiGateway::Resource'
    Properties:
      RestApiId: !Ref ApiGateway
      ParentId: !GetAtt ApiGateway.RootResourceId
      PathPart: 'publish'

  ApiResourceTopicLevel1:
    Type: 'AWS::ApiGateway::Resource'
    Properties:
      RestApiId: !Ref ApiGateway
      ParentId: !Ref ApiResourcePublish
      PathPart: '{topic_path_1}'

  ApiResourceTopicLevel2:
    Type: 'AWS::ApiGateway::Resource'
    Properties:
      RestApiId: !Ref ApiGateway
      ParentId: !Ref ApiResourceTopicLevel1
      PathPart: '{topic_path_2}'

  ApiMethod:
    Type: 'AWS::ApiGateway::Method'
    Properties:
      RestApiId: !Ref ApiGateway
      ResourceId: !Ref ApiResourceTopicLevel2
      HttpMethod: POST
      AuthorizationType: NONE
      Integration:
        Type: AWS
        IntegrationHttpMethod: POST
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:${AwsIotSubDomain}.iotdata:path/topics/{topic_level_1}/{topic_level_2}'
        Credentials: !GetAtt IoTRole.Arn
        RequestParameters:
          integration.request.path.topic_level_1: method.request.path.topic_path_1
          integration.request.path.topic_level_2: method.request.path.topic_path_2
          integration.request.querystring.qos: method.request.querystring.qos
        PassthroughBehavior: WHEN_NO_TEMPLATES
        IntegrationResponses:
          - StatusCode: 200
      RequestParameters:
        method.request.path.topic_path_1: true
        method.request.path.topic_path_2: true
        method.request.querystring.qos:  true
      MethodResponses:
        - StatusCode: 200

  ApiDeployment:
    Type: 'AWS::ApiGateway::Deployment'
    DependsOn: ApiMethod
    Properties:
      RestApiId: !Ref ApiGateway
      StageName: 'dev'

  IoTRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: apigateway.amazonaws.com
            Action: 'sts:AssumeRole'
      Policies:
        - PolicyName: IoTPublishPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: 'iot:Publish'
                Resource: !Sub 'arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/*'

Outputs:
  ApiEndpoint:
    Description: 'API Gateway endpoint URL for dev stage'
    Value: !Sub 'https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/dev/publish'

パターン 2 の動作確認

curl コマンドで手元の PC からリクエストを投げてみます。

curl -X POST "https://[YOUR-API-ID].execute-api.[YOUR-REGION].amazonaws.com/dev/publish/sensor/temperature?qos=1" -H "Content-Type: application/json" -d '{"data": "20.0"}'

AWS IoT Core 上のテストクライアントで sensor/temperature をサブスクライブしていれば、下記のようにメッセージを受信できました。

57-iot-core-test-client.png

API Gateway のコンソール上のテスト画面は、次のように 2 つのパス topic_path_1topic_path_2 (とクエリ文字列 qos=1)を指定する形になります。

58-path-topic-test.png

テストを実行すると次のようなログを確認ました。指定したパスが正しく展開されていることが分かります。

Method request path: {topic_path_1=sensor, topic_path_2=temperature}
Method request query string: {qos=1}
Method request body before transformations: {"data": "20.0"}
Endpoint request URI: https://[YOUR-IOT-CORE-ENDPOINT].iot.ap-northeast-1.amazonaws.com/topics/sensor/temperature?qos=1

ちなみに今回は 2 階層のトピックに送る前提ですが、2 階層以上のトピックを使いたい場合は適宜 API Gateway の設定を追加してください。

パターン 3. Publish するトピックを クエリ文字列で任意に指定する場合

最後に、送信するトピックをクエリ文字列として送る場合です。

102-http-publish-url-query-string-pattern.png

QoS と 2 階層分のトピックをクエリ文字列にセットしたいので、「メソッドリクエスト」は次のようにセットします。

  • URL クエリ文字列パラメータ
    • qos
    • topic_querystring_1
    • topic_querystring_2

59-query-string-params.png

また、結合リクエストの「パスオーバーライド」はこれまでと同じ topics/{topic_level_1}/{topic_level_2} として、各パスパラメータ({topic_level_1}{topic_level_2})を先程のクエリ文字列にマッピングしたいので、パスパラメータを次のように設定します。

  • URL パスパラメータ
名前 マッピング元
topic_level_1 method.request.querystring.topic_querystring_1
topic_level_2 method.request.querystring.topic_querystring_2
  • URL クエリ文字列パラメータ
名前 マッピング元
qos method.request.querystring.qos

60-integration-requests.png

パターン 3 の CloudFormation テンプレート

この CloudFormation テンプレートは以下のとおりです。

AWSTemplateFormatVersion: '2010-09-09'
Description: 'API Gateway with AWS IoT Core integration without mapping template'

Parameters:
  AwsIotSubDomain:
    Type: String
    Default: ""

Resources:
  ApiGateway:
    Type: 'AWS::ApiGateway::RestApi'
    Properties:
      Name: 'IoTCoreIntegrationApiQueryString'
      Description: 'API for publishing messages to AWS IoT Core'
      EndpointConfiguration:
        Types:
          - REGIONAL

  ApiResource:
    Type: 'AWS::ApiGateway::Resource'
    Properties:
      RestApiId: !Ref ApiGateway
      ParentId: !GetAtt ApiGateway.RootResourceId
      PathPart: 'publish'
  ApiMethod:
    Type: 'AWS::ApiGateway::Method'
    Properties:
      RestApiId: !Ref ApiGateway
      ResourceId: !Ref ApiResource
      HttpMethod: POST
      AuthorizationType: NONE
      Integration:
        Type: AWS
        IntegrationHttpMethod: POST
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:${AwsIotSubDomain}.iotdata:path/topics/{topic_level_1}/{topic_level_2}'
        Credentials: !GetAtt IoTRole.Arn
        RequestParameters:
          integration.request.path.topic_level_1: method.request.querystring.topic_querystring_1
          integration.request.path.topic_level_2: method.request.querystring.topic_querystring_2
          integration.request.querystring.qos: method.request.querystring.qos
        PassthroughBehavior: WHEN_NO_TEMPLATES
        IntegrationResponses:
          - StatusCode: 200
      RequestParameters:
        method.request.querystring.qos:  true
        method.request.querystring.topic_querystring_1: true
        method.request.querystring.topic_querystring_2: true
      MethodResponses:
        - StatusCode: 200

  ApiDeployment:
    Type: 'AWS::ApiGateway::Deployment'
    DependsOn: ApiMethod
    Properties:
      RestApiId: !Ref ApiGateway
      StageName: 'dev'

  IoTRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: apigateway.amazonaws.com
            Action: 'sts:AssumeRole'
      Policies:
        - PolicyName: IoTPublishPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: 'iot:Publish'
                Resource: !Sub 'arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/*'

Outputs:
  ApiEndpoint:
    Description: 'API Gateway endpoint URL for dev stage'
    Value: !Sub 'https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/dev/publish'

パターン 3 の動作確認

curl コマンドで手元の PC からリクエストを投げてみます。

curl -X POST "https://[YOUR-API-ID].execute-api.[YOUR-REGION].amazonaws.com/dev/publish/?topic_querystring_1=sensor&topic_querystring_2=temperature&qos=1" -H "Content-Type: application/json" -d '{"data": "25.5"}'

AWS IoT Core 上のテストクライアントで sensor/temperature をサブスクライブしていれば、下記のようにメッセージを受信できました。

61-iot-core-test-client.png

API Gateway のコンソール上のテスト画面では、次のようにクエリ文字列を指定する形になります。

topic_querystring_1=sensor&topic_querystring_2=temperature&qos=1

62-query-string-topic-test.png

テストを実行すると次のようなログを確認ました。指定したクエリ文字列が正しく変換されていることが分かります。

Method request path: {}
Method request query string: {topic_querystring_2=temperature, qos=1, topic_querystring_1=sensor}
Method request body before transformations: {"data": "25.5"}
Endpoint request URI: https://[YOUR-IOT-Core-ENDPOINT].iot.ap-northeast-1.amazonaws.com/topics/sensor/temperature?qos=1

最後に

API Gateway から IoT Core に対するメッセージの送信方法を 3 パターン見てきました。送り方はそれぞれ違えど最終的にすべて同じ IoT Core の HTTP エンドポイント [IoT Core Endpoint]/topics/sensor/temperature?qos=1 に送れました。

利用頻度は多くないかもしれませんが、マッチしそうなユースケースがあればぜひ活用していただければと思います。

参考 URL

https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/http.html
https://docs.aws.amazon.com/iot/latest/developerguide/protocols.html
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-call-api.html
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/integration-request-basic-setup.html
https://dev.classmethod.jp/articles/put-together-about-the-relationship-between-cfn-and-management-console-items/

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.